All files / src/app/api/support/tickets/[id]/close route.ts

0% Statements 0/95
100% Branches 0/0
0% Functions 0/1
0% Lines 0/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                                                                                                                                                               
export const dynamic = "force-dynamic";

/**
 * Close Support Ticket API
 * POST /api/support/tickets/[id]/close - Close a ticket (customer)
 */

import { NextRequest, NextResponse } from "next/server";
import { Session } from "next-auth";
import { prisma } from "@/lib/prisma";
import { recordStatusChange } from "@/lib/support/ticket-utils";
import { TicketStatus } from "@/types/support";
import { logger } from "@/lib/logging";
import {
  withErrorHandling,
  withAuth,
  successResponse,
  ApiError } from "@/lib/api";
import { RouteContext } from "@/lib/api/middleware";

/**
 * POST /api/support/tickets/[id]/close
 * Close a ticket (customer can close their own tickets)
 */
async function handlePost(
  _request: NextRequest,
  context: RouteContext | undefined,
  session: Session
): Promise<NextResponse> {
  if (!context?.params) {
    throw ApiError.invalidId("ticket");
  }

  const { id } = await context.params;
  const userId = Number(session.user.id);

  // Verify ticket exists and belongs to user
  const ticket = await prisma.supportTicket.findUnique({
    where: { id },
    select: { id: true, userId: true, status: true, ticketNumber: true }});

  if (!ticket) {
    throw ApiError.notFound("Ticket", id);
  }

  if (ticket.userId !== userId) {
    throw ApiError.forbidden("Access denied");
  }

  // Check if ticket can be closed
  const closableStatuses = [
    TicketStatus.OPEN,
    TicketStatus.IN_PROGRESS,
    TicketStatus.AWAITING_CUSTOMER,
    TicketStatus.AWAITING_AGENT,
    TicketStatus.RESOLVED,
  ];

  if (!closableStatuses.includes(ticket.status as TicketStatus)) {
    throw ApiError.validation("This ticket cannot be closed");
  }

  const oldStatus = ticket.status as TicketStatus;

  // Close the ticket
  const updatedTicket = await prisma.supportTicket.update({
    where: { id },
    data: {
      status: TicketStatus.CLOSED,
      closedAt: new Date()}});

  // Record status change
  await recordStatusChange(id, oldStatus, TicketStatus.CLOSED, userId);

  // Add system message
  await prisma.supportMessage.create({
    data: {
      ticketId: id,
      senderType: "SYSTEM",
      senderName: "System",
      content: "Ticket closed by customer.",
      isInternal: false}});

  logger.info("Ticket closed by customer", {
    category: "API",
    ticketId: id,
    ticketNumber: ticket.ticketNumber,
    userId});

  return successResponse({
    ...updatedTicket,
    message: "Ticket closed successfully"});
}

export const POST = withErrorHandling(withAuth(handlePost));